Docker Layer

도커 이미지를 하나 빌드하고 살펴보면

docker build -f Dockerfile.test -t test-image:latest .
docker inspect test-image:latest

아래처럼 Layer가 존재한다.

"RootFS": {
    "Type": "layers",
    "Layers": [
        "sha256:0e64f2360a448b389c83fcbc3705e1364ccf43f004bb55233c689e7ce59a1ae9",
        "sha256:c428b24b6e21aac376b03626de5664ce169bd0c378f01307899e0b2376bdd7fb",
        "sha256:47d417943a7a86c5c9fba64536563c360326cd2034ad12bfe7bc684509f3b9cb",
        "sha256:acea1d94d1551e9e3007f8583028fd9457a3638f736151db9c085096a28df9d0",
        "sha256:76d999dad88191f63e0a6c58a7e86fa02a68a8f83ba72b8693d4ca2a2bbd0dbf",
        "sha256:18690e1e26f1c0745f30a549d5caea4e8b211c50d0e0ab48b83ec7df9335d192",
        "sha256:d9605615e586914b25cad8a98465574e7696f01675dd93a1dd47bc7afd7c94c3",
        "sha256:61b8b085399c264d288470ffd65ed34dc1feca2e8be6482b89aff2d2288da012"
    ]
},
"Metadata": {
    "LastTagTime": "2025-11-26T14:51:38.063951839Z"
},

Docker Image를 그냥 하나의 큰 파일이라고 생각할 수 있지만,
실제로, Image는 Layer의 Stack으로 이뤄진 파일이다.

Union File System


union-fs


Docker Image는 여러 개의 Layer로 이루어져 있다.
그런데 컨테이너를 실행할 때 Layer별 디렉토리를 전부 별도로 다루면 너무 복잡하니,
여러 개의 디렉토리(=Layer)를 하나의 디렉토리처럼 합쳐서 보여주는 파일시스템 기술인 Union FS가 적용되어 있다.

스포를 하자면, Layer는 DockerFile 명령어마다 하나씩 생성된다.

# Dockerfile 명령어에 따라 Layer 생성
Layer 3  ←  RUN npm install
Layer 2  ←  COPY src .
Layer 1  ←  FROM node:20-alpine

# Layer마다 물리적으로 디렉터리가 다름
/var/lib/docker/overlay2/<layer1>/diff
/var/lib/docker/overlay2/<layer2>/diff
/var/lib/docker/overlay2/<layer3>/diff
...

각 Layer는 실제로는 물리적으로 서로 다른 디렉터리이지만
컨테이너 실행시에는 하나의 Union FS 덕분에 하나의 파일로 처리된다.

왜 하나의 파일로 처리될까?

각 Layer는 물리적으로는 서로 다른 디렉터리이지만,
컨테이너 실행 시에는 Union FS가 이 Layer들을 하나의 루트 파일시스템으로 병합해준다.

/
├── bin
├── usr
├── lib
└── app

리눅스 프로세스는 단 하나의 루트 파일시스템(/)에서만 실행될 수 있다.
즉, /bin, /usr, /app 등이 단일 트리 구조로 존재해야 하며,
Docker는 여러 레이어를 그대로 보여줄 수 없기 때문에 UnionFS로 통합한다.


Docker Layer

Dockerfile의 각 명령어는 독립적인 Layer를 생성한다.
이때 Layer는 기존 Layer 전체를 복사하지 않고, 변경된 파일 시스템의 차이 만 포함한다.
즉, “현재 명령어로 인해 새로 추가·변경·삭제된 파일만” 새로운 Layer로 기록된다.

Layer들은 Union File System(OverlayFS 등)에 의해 위에서 아래로 합쳐져,
겉보기에는 하나의 완전한 파일 시스템처럼 보인다.

Docker는 왜 레이어를 분리했을까

캐싱-재사용을 통한, 저장 공간 절약과 빌드 시간 단축 때문이다.

COPY package.json .
RUN pnpm install

일반적으로 애플리케이션 코드는 자주 바뀌지만, 의존성 설치(pnpm install) 단계는 자주 바뀌지 않는다.

Layer가 있다면, Docker는 이전에 생성해둔 “의존성 설치 레이어”를 그대로 재사용하고, 그 이후 단계만 다시 빌드하면 된다. 캐시 히트가 발생하면 수 분 걸리던 설치 작업이 즉시 스킵된다.

참고. Docker Readonly-Layer와 Writable Layer

Docker Image의 모든 Layer는 기본적으로 Read-only이다.
컨테이너가 실행될 때 Docker는 최상단에 Writable Layer(컨테이너 레이어) 를 추가한다.

뭔가가 실행될 때, 로그를 남기거나, runtime에 파일 변경등이 필요해질텐데, 그것을 위한 공간이다. 이미지 레이어는 절대 수정되지 않고, 컨테이너가 종료되면 Writable Layer만 폐기된다. 이로 인해 이미지의 재현성(reproducibility)과 안전성이 유지된다.

Docker Layer Cache 는 어떻게 동작할까

Docker 이미지를 생성할 때, Dockerfile의 각 명령어는 독립적인 Layer를 생성하면서 이미지를 만들어낸다.

FROM node:22-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "./src/index.js"]

실제로 빌드될 때는 이렇게 Layer가 생긴다고 이해하면 된다:

1. FROM → base image layer
2. WORKDIR /app → workdir 설정 layer
3. COPY . . → 애플리케이션 파일 layer
4. RUN yarn install → dependencies 설치 layer
5. CMD ... → 메타데이터 layer (실행 명령)

이렇게 이미지를 생성하기 위해, 하나의 Layer를 쌓을 때 이전 빌드 결과(= Layer 캐시)를 재사용하려고 시도한다.
이때 아래와 같은 기준으로 Layer Cache를 재사용 가능한지 판단한다.

  1. RUN 명령어가 바뀌면 다른 캐시다.

  2. COPY/ADD로 복사하는 대상의 파일의 메타데이터가 바뀌면 캐시가 무효화된다.

    여기서 말하는 메타데이터는 파일크기, 체크섬, inode등이 있다. 대부분 캐시가 무효화 되는 것은, 소스코드 파일 전체를 Copy 할 때
    들어오는 파일의 형상이 달라지기 때문이다.

  3. 앞선 레이어가 무효화 되면, 뒤에 쌓이는 모든 레이어는 전부 무효화된다.

    Stack 형태의 Layer들이 하나의 파일로 합쳐지는 것이기 때문에, 하나의 결과가 달라지면
    뒤 레이어 생성시의 Input 값이 달라지는 것과 같기 때문에, 모든 캐시가 무효화된다.

이 재사용 조건을 염두하고 Dockerfile을 작성하면, 이미지 생성 시간을 단축할 수 있다.


마무리

Docker Image는 여러 Layer가 쌓여 만들어지고, 각 Layer는 Dockerfile 명령어와 파일 변화(Diff)를 기반으로 캐시된다. 이 구조 덕분에 Docker는 빠른 빌드, 저장공간 절약, 재현성을 동시에 얻는다.

Layer 구조와 캐시 무효화 조건을 이해하면 “어떤 단계가 캐시되고, 어떤 단계가 매번 다시 빌드되는지”를 정확히 예측할 수 있고, Dockerfile을 훨씬 효율적으로 작성할 수 있다.

다음 글에서는 실제로 Build Cache를 활용하는 Dockerfile 패턴과 CI/CD에서 캐시를 제대로 활용하는 방법을 정리해보겠다.


References